@owomark/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -0
- package/dist/adapter/dom-adapter.d.ts +1 -0
- package/dist/adapter/dom-adapter.js +261 -0
- package/dist/chunk-RYTHPR7H.js +3259 -0
- package/dist/dom-adapter-IZEW91gZ.d.ts +463 -0
- package/dist/index.d.ts +364 -0
- package/dist/index.js +640 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# @owomark/core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic Markdown editor engine. Provides document model, incremental rendering, input handling, commands, and selection management for a single-layer `contenteditable` editing experience.
|
|
4
|
+
|
|
5
|
+
Official package: `@owomark/core`. The flat name `owomark-core` is reserved only as a compatibility redirect and should not be used for new installs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @owomark/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Pure Logic Core (no DOM)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createOwoMarkCore } from '@owomark/core';
|
|
19
|
+
|
|
20
|
+
const core = createOwoMarkCore({ initialMarkdown: '# Hello' });
|
|
21
|
+
|
|
22
|
+
// Intent-driven input API
|
|
23
|
+
const result = core.applyBeforeInput(
|
|
24
|
+
{ inputType: 'insertText', data: 'X' },
|
|
25
|
+
{ anchor: 7, focus: 7 },
|
|
26
|
+
);
|
|
27
|
+
// result.action: 'handled' | 'allow-native' | 'composition-sync'
|
|
28
|
+
|
|
29
|
+
console.log(core.getMarkdown()); // '# HelloX'
|
|
30
|
+
core.destroy();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### With DOM (use @owomark/view)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createOwoMarkCore } from '@owomark/core';
|
|
37
|
+
import { createOwoMarkView } from '@owomark/view';
|
|
38
|
+
|
|
39
|
+
const core = createOwoMarkCore({ initialMarkdown: '# Hello\n\nStart typing...' });
|
|
40
|
+
const view = createOwoMarkView(core, document.getElementById('editor')!);
|
|
41
|
+
|
|
42
|
+
core.onChange((markdown) => {
|
|
43
|
+
console.log('Content changed:', markdown);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Cleanup
|
|
47
|
+
view.destroy();
|
|
48
|
+
core.destroy();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For backward compatibility, a legacy standalone editor API is also available:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { createOwoMarkVanillaEditor } from '@owomark/view';
|
|
55
|
+
|
|
56
|
+
const editor = createOwoMarkVanillaEditor();
|
|
57
|
+
editor.setMarkdown('# Hello');
|
|
58
|
+
editor.mount(document.getElementById('editor')!);
|
|
59
|
+
editor.destroy();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Core API
|
|
63
|
+
|
|
64
|
+
### State
|
|
65
|
+
|
|
66
|
+
| Method | Description |
|
|
67
|
+
|--------|-------------|
|
|
68
|
+
| `getMarkdown(): string` | Get current Markdown content |
|
|
69
|
+
| `getDocument(): OwoMarkDocument` | Get immutable document model |
|
|
70
|
+
| `getSelection(): OwoMarkSelection \| null` | Current selection as linear offsets |
|
|
71
|
+
| `getVirtualSelection()` | Current selection as `{ blockId, offset }` pairs |
|
|
72
|
+
| `getComposition()` | Current IME composition state |
|
|
73
|
+
| `getSnapshot()` | Full state snapshot |
|
|
74
|
+
|
|
75
|
+
### Input Handling
|
|
76
|
+
|
|
77
|
+
| Method | Description |
|
|
78
|
+
|--------|-------------|
|
|
79
|
+
| `applyBeforeInput(intent, selection)` | Handle `beforeinput` events |
|
|
80
|
+
| `applyKeyDown(intent, selection)` | Handle `keydown` events |
|
|
81
|
+
| `applyPaste(intent, selection)` | Handle `paste` events |
|
|
82
|
+
| `handleCompositionStart(selection)` | Enter IME composition mode |
|
|
83
|
+
| `handleCompositionEnd(selection)` | Exit IME composition mode |
|
|
84
|
+
| `syncText(text, selection)` | Sync DOM text back to model (IME fallback) |
|
|
85
|
+
|
|
86
|
+
### Content Mutation
|
|
87
|
+
|
|
88
|
+
| Method | Description |
|
|
89
|
+
|--------|-------------|
|
|
90
|
+
| `setMarkdown(md: string)` | Replace content (resets history) |
|
|
91
|
+
| `replaceMarkdown(md, selection)` | Replace content with explicit selection (supports undo) |
|
|
92
|
+
| `dispatch(command, payload?)` | Dispatch a named command |
|
|
93
|
+
|
|
94
|
+
### Events
|
|
95
|
+
|
|
96
|
+
| Method | Description |
|
|
97
|
+
|--------|-------------|
|
|
98
|
+
| `onChange(cb)` | Subscribe to content changes |
|
|
99
|
+
| `onSelectionChange(cb)` | Subscribe to selection changes |
|
|
100
|
+
| `onCompositionStateChange(cb)` | Subscribe to IME composition state |
|
|
101
|
+
| `onDocumentChange(cb)` | Subscribe to document model changes (sovereign mode) |
|
|
102
|
+
| `onStateChange(cb)` | Subscribe to any state change |
|
|
103
|
+
|
|
104
|
+
## Pure Function Exports
|
|
105
|
+
|
|
106
|
+
The package also exports pure functions that can be used independently:
|
|
107
|
+
|
|
108
|
+
### Commands
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import {
|
|
112
|
+
handleMarkdownEnter,
|
|
113
|
+
applyMarkdownIndent,
|
|
114
|
+
handleCharInput,
|
|
115
|
+
handleSmartBackspace,
|
|
116
|
+
toggleBold,
|
|
117
|
+
toggleItalic,
|
|
118
|
+
insertLink,
|
|
119
|
+
insertCodeFence,
|
|
120
|
+
} from '@owomark/core';
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Parser
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { tokenizeBlock, tokenizeInline } from '@owomark/core';
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Model
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import {
|
|
133
|
+
parseMarkdownToDocument,
|
|
134
|
+
serializeDocument,
|
|
135
|
+
getBlockAtOffset,
|
|
136
|
+
} from '@owomark/core';
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Clipboard
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { normalizeMarkdownPaste } from '@owomark/core';
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Supported Markdown
|
|
146
|
+
|
|
147
|
+
- Headings (`# ` through `###### `)
|
|
148
|
+
- Bold (`**text**`) and italic (`*text*`)
|
|
149
|
+
- Inline code (`` `code` ``)
|
|
150
|
+
- Links (`[text](url)`)
|
|
151
|
+
- Unordered and ordered lists
|
|
152
|
+
- Blockquotes (`> `)
|
|
153
|
+
- Fenced code blocks (`` ``` ``)
|
|
154
|
+
- Thematic breaks (`---`)
|
|
155
|
+
|
|
156
|
+
## Preview Projection
|
|
157
|
+
|
|
158
|
+
Convert a parsed document into preview-ready blocks for incremental rendering:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import {
|
|
162
|
+
createSharedStateStore,
|
|
163
|
+
projectToPreviewBlocks,
|
|
164
|
+
computePreviewDirtyRange,
|
|
165
|
+
deriveBlockId,
|
|
166
|
+
} from '@owomark/core';
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Shared State Store
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const store = createSharedStateStore({ initialMarkdown: '# Hello' });
|
|
173
|
+
|
|
174
|
+
// Connect an editor instance
|
|
175
|
+
const disconnect = store.connectEditor(editorInstance);
|
|
176
|
+
|
|
177
|
+
// Subscribe to state changes (consumed by preview engines)
|
|
178
|
+
const unsubscribe = store.subscribe((state) => {
|
|
179
|
+
console.log('Version:', state.version);
|
|
180
|
+
console.log('Preview blocks:', state.previewBlocks.length);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Cleanup
|
|
184
|
+
unsubscribe();
|
|
185
|
+
disconnect();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Block Projection
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const blocks = projectToPreviewBlocks(document, 'vitesse-light');
|
|
192
|
+
const dirtyRange = computePreviewDirtyRange(previousBlocks, blocks);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Adjacent same-type container blocks (lists, blockquotes) are grouped into a single `PreviewBlock`
|
|
196
|
+
- Each block has a stable `blockId` (`L{startLine}-{endLine}`) for DOM reconciliation
|
|
197
|
+
- `renderKey` (djb2 hash of `kind:theme:raw`) drives cache invalidation
|
|
198
|
+
|
|
199
|
+
### Preview Types
|
|
200
|
+
|
|
201
|
+
- `PreviewBlock` — block identity, kind, raw content, line range, render key
|
|
202
|
+
- `PreviewBlockKind` — `'paragraph' | 'heading' | 'code-fence' | ...`
|
|
203
|
+
- `PreviewDirtyReason` — `'text-edit' | 'structure-edit' | 'theme-change' | 'renderer-change' | 'custom-block-change' | 'full-rebuild'`
|
|
204
|
+
- `OwoMarkSharedState` — full shared state snapshot (document, preview blocks, selection, version)
|
|
205
|
+
- `OwoMarkSharedStateStore` — subscribe/getState store interface
|
|
206
|
+
- `OwoMarkSharedStateController` — store + `setMarkdown()`, `connectEditor()`, `updateVisibleBlockIds()`, `setThemeKey()`, `notifyRendererChange()`, `notifyCustomBlockChange()`
|
|
207
|
+
|
|
208
|
+
## Design Principles
|
|
209
|
+
|
|
210
|
+
- **Input-first**: IME composition is never interrupted by rendering. Patches only run after `compositionend`.
|
|
211
|
+
- **Incremental**: Only dirty blocks are re-parsed and re-rendered. No full DOM rebuilds on each keystroke.
|
|
212
|
+
- **Framework-free**: Zero runtime dependencies. Works with any UI framework or vanilla JS.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { v as DocumentChangeCallback, w as DomAdapterInstance, a6 as createDomAdapter } from '../dom-adapter-IZEW91gZ.js';
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createOwoMarkCore,
|
|
3
|
+
detectAndRenderDirty,
|
|
4
|
+
fullRender,
|
|
5
|
+
readSelection,
|
|
6
|
+
restoreSelection
|
|
7
|
+
} from "../chunk-RYTHPR7H.js";
|
|
8
|
+
|
|
9
|
+
// src/adapter/dom-adapter.ts
|
|
10
|
+
function createDomAdapter() {
|
|
11
|
+
const core = createOwoMarkCore();
|
|
12
|
+
let root = null;
|
|
13
|
+
let compositionJustEnded = false;
|
|
14
|
+
let documentChangeCallback = null;
|
|
15
|
+
function syncEmptyAttr() {
|
|
16
|
+
if (!root) return;
|
|
17
|
+
const doc = core.getDocument();
|
|
18
|
+
const empty = doc.blocks.length === 0 || doc.blocks.length === 1 && doc.blocks[0].raw === "";
|
|
19
|
+
if (empty) {
|
|
20
|
+
root.setAttribute("data-owo-empty", "true");
|
|
21
|
+
} else {
|
|
22
|
+
root.removeAttribute("data-owo-empty");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function syncFromDOM() {
|
|
26
|
+
if (!root) return;
|
|
27
|
+
const textContent = extractTextFromRoot(root);
|
|
28
|
+
const sel = readSelection(root);
|
|
29
|
+
const oldMarkdown = core.getMarkdown();
|
|
30
|
+
if (textContent !== oldMarkdown) {
|
|
31
|
+
const oldDoc = core.getDocument();
|
|
32
|
+
core.syncText(textContent, sel);
|
|
33
|
+
if (!documentChangeCallback && root) {
|
|
34
|
+
const newDoc = core.getDocument();
|
|
35
|
+
detectAndRenderDirty(root, oldDoc, newDoc, true);
|
|
36
|
+
if (sel) {
|
|
37
|
+
restoreSelection(root, sel);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
syncEmptyAttr();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function onBeforeInput(e) {
|
|
44
|
+
if (core.getComposition().active) return;
|
|
45
|
+
const sel = readSelection(root);
|
|
46
|
+
const oldDoc = core.getDocument();
|
|
47
|
+
const result = core.applyBeforeInput(
|
|
48
|
+
{ inputType: e.inputType, data: e.data },
|
|
49
|
+
sel
|
|
50
|
+
);
|
|
51
|
+
if (result.action === "handled") {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
if (!documentChangeCallback && root) {
|
|
54
|
+
const newDoc = core.getDocument();
|
|
55
|
+
if (oldDoc !== newDoc) {
|
|
56
|
+
detectAndRenderDirty(root, oldDoc, newDoc);
|
|
57
|
+
const linearSel = core.getSelection();
|
|
58
|
+
if (linearSel) restoreSelection(root, linearSel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
syncEmptyAttr();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function onInput() {
|
|
65
|
+
if (core.getComposition().active) return;
|
|
66
|
+
if (compositionJustEnded) {
|
|
67
|
+
compositionJustEnded = false;
|
|
68
|
+
syncFromDOM();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function onKeyDown(e) {
|
|
72
|
+
if (core.getComposition().active) return;
|
|
73
|
+
const sel = readSelection(root);
|
|
74
|
+
const oldDoc = core.getDocument();
|
|
75
|
+
const result = core.applyKeyDown(
|
|
76
|
+
{
|
|
77
|
+
key: e.key,
|
|
78
|
+
ctrlKey: e.ctrlKey,
|
|
79
|
+
metaKey: e.metaKey,
|
|
80
|
+
altKey: e.altKey,
|
|
81
|
+
shiftKey: e.shiftKey
|
|
82
|
+
},
|
|
83
|
+
sel
|
|
84
|
+
);
|
|
85
|
+
if (result.action === "handled") {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
if (!documentChangeCallback && root) {
|
|
88
|
+
const newDoc = core.getDocument();
|
|
89
|
+
if (oldDoc !== newDoc) {
|
|
90
|
+
detectAndRenderDirty(root, oldDoc, newDoc);
|
|
91
|
+
const linearSel = core.getSelection();
|
|
92
|
+
if (linearSel) restoreSelection(root, linearSel);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
syncEmptyAttr();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function onPaste(e) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
const raw = e.clipboardData?.getData("text/plain") ?? "";
|
|
101
|
+
const sel = readSelection(root);
|
|
102
|
+
const oldDoc = core.getDocument();
|
|
103
|
+
const result = core.applyPaste({ text: raw }, sel);
|
|
104
|
+
if (result.action === "handled" && !documentChangeCallback && root) {
|
|
105
|
+
const newDoc = core.getDocument();
|
|
106
|
+
if (oldDoc !== newDoc) {
|
|
107
|
+
detectAndRenderDirty(root, oldDoc, newDoc);
|
|
108
|
+
const linearSel = core.getSelection();
|
|
109
|
+
if (linearSel) restoreSelection(root, linearSel);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
syncEmptyAttr();
|
|
113
|
+
}
|
|
114
|
+
function onCompositionStart() {
|
|
115
|
+
const sel = readSelection(root);
|
|
116
|
+
core.handleCompositionStart(sel);
|
|
117
|
+
}
|
|
118
|
+
function onCompositionUpdate() {
|
|
119
|
+
core.handleCompositionUpdate();
|
|
120
|
+
}
|
|
121
|
+
function onCompositionEnd(_e) {
|
|
122
|
+
const sel = readSelection(root);
|
|
123
|
+
core.handleCompositionEnd(sel);
|
|
124
|
+
compositionJustEnded = true;
|
|
125
|
+
requestAnimationFrame(() => {
|
|
126
|
+
if (compositionJustEnded) {
|
|
127
|
+
compositionJustEnded = false;
|
|
128
|
+
if (root) syncFromDOM();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function onSelectionChange() {
|
|
133
|
+
if (!root) return;
|
|
134
|
+
const sel = readSelection(root);
|
|
135
|
+
if (sel) {
|
|
136
|
+
core.updateSelection(sel);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let selectionObserver = null;
|
|
140
|
+
function mount(element) {
|
|
141
|
+
root = element;
|
|
142
|
+
root.contentEditable = "true";
|
|
143
|
+
root.setAttribute("role", "textbox");
|
|
144
|
+
root.setAttribute("aria-multiline", "true");
|
|
145
|
+
root.setAttribute("aria-label", "Markdown editor");
|
|
146
|
+
root.classList.add("owo-editor-root");
|
|
147
|
+
root.style.whiteSpace = "pre-wrap";
|
|
148
|
+
root.style.wordBreak = "break-word";
|
|
149
|
+
root.style.outline = "none";
|
|
150
|
+
if (!documentChangeCallback) {
|
|
151
|
+
fullRender(root, core.getDocument());
|
|
152
|
+
}
|
|
153
|
+
syncEmptyAttr();
|
|
154
|
+
root.addEventListener("beforeinput", onBeforeInput);
|
|
155
|
+
root.addEventListener("input", onInput);
|
|
156
|
+
root.addEventListener("compositionstart", onCompositionStart);
|
|
157
|
+
root.addEventListener("compositionupdate", onCompositionUpdate);
|
|
158
|
+
root.addEventListener("compositionend", onCompositionEnd);
|
|
159
|
+
root.addEventListener("keydown", onKeyDown);
|
|
160
|
+
root.addEventListener("paste", onPaste);
|
|
161
|
+
const onDocSelectionChange = () => onSelectionChange();
|
|
162
|
+
root.ownerDocument.addEventListener("selectionchange", onDocSelectionChange);
|
|
163
|
+
selectionObserver = () => {
|
|
164
|
+
root?.ownerDocument.removeEventListener("selectionchange", onDocSelectionChange);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function destroy() {
|
|
168
|
+
if (!root) return;
|
|
169
|
+
root.removeEventListener("beforeinput", onBeforeInput);
|
|
170
|
+
root.removeEventListener("input", onInput);
|
|
171
|
+
root.removeEventListener("compositionstart", onCompositionStart);
|
|
172
|
+
root.removeEventListener("compositionupdate", onCompositionUpdate);
|
|
173
|
+
root.removeEventListener("compositionend", onCompositionEnd);
|
|
174
|
+
root.removeEventListener("keydown", onKeyDown);
|
|
175
|
+
root.removeEventListener("paste", onPaste);
|
|
176
|
+
selectionObserver?.();
|
|
177
|
+
selectionObserver = null;
|
|
178
|
+
core.destroy();
|
|
179
|
+
root = null;
|
|
180
|
+
}
|
|
181
|
+
function focus() {
|
|
182
|
+
root?.focus();
|
|
183
|
+
}
|
|
184
|
+
function setMarkdown(markdown) {
|
|
185
|
+
core.setMarkdown(markdown);
|
|
186
|
+
if (!documentChangeCallback && root) {
|
|
187
|
+
fullRender(root, core.getDocument());
|
|
188
|
+
const linearSel = core.getSelection();
|
|
189
|
+
if (linearSel) restoreSelection(root, linearSel);
|
|
190
|
+
}
|
|
191
|
+
syncEmptyAttr();
|
|
192
|
+
}
|
|
193
|
+
function replaceMarkdown(markdown, selection) {
|
|
194
|
+
const oldDoc = core.getDocument();
|
|
195
|
+
core.replaceMarkdown(markdown, selection);
|
|
196
|
+
if (!documentChangeCallback && root) {
|
|
197
|
+
detectAndRenderDirty(root, oldDoc, core.getDocument());
|
|
198
|
+
restoreSelection(root, selection);
|
|
199
|
+
}
|
|
200
|
+
syncEmptyAttr();
|
|
201
|
+
}
|
|
202
|
+
function dispatch(command, payload) {
|
|
203
|
+
const oldDoc = core.getDocument();
|
|
204
|
+
core.dispatch(command, payload);
|
|
205
|
+
if (!documentChangeCallback && root) {
|
|
206
|
+
const newDoc = core.getDocument();
|
|
207
|
+
if (oldDoc !== newDoc) {
|
|
208
|
+
if (command === "undo" || command === "redo") {
|
|
209
|
+
fullRender(root, newDoc);
|
|
210
|
+
} else {
|
|
211
|
+
detectAndRenderDirty(root, oldDoc, newDoc);
|
|
212
|
+
}
|
|
213
|
+
const linearSel = core.getSelection();
|
|
214
|
+
if (linearSel) restoreSelection(root, linearSel);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
syncEmptyAttr();
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
mount,
|
|
221
|
+
destroy,
|
|
222
|
+
getMarkdown: () => core.getMarkdown(),
|
|
223
|
+
setMarkdown,
|
|
224
|
+
replaceMarkdown,
|
|
225
|
+
getSelection: () => core.getSelection(),
|
|
226
|
+
getDocument: () => core.getDocument(),
|
|
227
|
+
focus,
|
|
228
|
+
dispatch,
|
|
229
|
+
setIndentMode: (mode) => core.setIndentMode(mode),
|
|
230
|
+
onChange: (callback) => core.onChange(callback),
|
|
231
|
+
onSelectionChange: (callback) => core.onSelectionChange(callback),
|
|
232
|
+
onCompositionStateChange: (callback) => core.onCompositionStateChange(callback),
|
|
233
|
+
onDocumentChange(callback) {
|
|
234
|
+
documentChangeCallback = callback;
|
|
235
|
+
const unsub = core.onDocumentChange(callback);
|
|
236
|
+
return () => {
|
|
237
|
+
unsub();
|
|
238
|
+
if (documentChangeCallback === callback) {
|
|
239
|
+
documentChangeCallback = null;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
getCore: () => core
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function extractTextFromRoot(root) {
|
|
247
|
+
const parts = [];
|
|
248
|
+
for (const child of root.childNodes) {
|
|
249
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
250
|
+
const el = child;
|
|
251
|
+
if (el.hasAttribute("data-owo-block")) {
|
|
252
|
+
if (parts.length > 0) parts.push("\n");
|
|
253
|
+
parts.push(el.textContent ?? "");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return parts.join("");
|
|
258
|
+
}
|
|
259
|
+
export {
|
|
260
|
+
createDomAdapter
|
|
261
|
+
};
|